#include <ncurses.h>
#include <errno.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#define RED_ON_BLACK 1
#define GREEN_ON_BLACK 2
#define BLUE_ON_BLACK 3
#define YELLOW_ON_BLACK 4
#define PINK_ON_BLACK 5
#define CYAN_ON_BLACK 6
#define WHITE_ON_RED 7
#define WHITE_ON_BLACK 8

#define SCREEN_NUMBER 6
#define FRAME 0
#define MAIN_MENU 1
#define INPUT_CHOICE 2
#define MANUAL_INPUT 3
#define MODEL_CHOICE 4
#define SIMULATION 5

#define MAIN_MENU_OPTIONS 2
#define INPUT_OPTIONS 4 
#define MODEL_OPTIONS 5 

#define TAB_SIZE 20
#define LIVE_CELL '+'
#define DEAD_CELL ' '

#define ENTER 13
#define UP 259
#define DOWN 258
#define ESCAPE 27
#define SPACE 32

typedef struct Cell 
{
   int x;
   int y;
} Cell;

void error(char* message, int errorID);

void validateColors();
void initColorPairs();

void createScreens(WINDOW* scrList[SCREEN_NUMBER]);
void buildFrame(WINDOW* scr);
void buildMainMenu(WINDOW* scr, int selectedIndex);
void buildInputMenu(WINDOW* scr, int selectedIndex);
void buildModelMenu(WINDOW* scr, int selectedIndex);
void buildGenericMenu(WINDOW* scr, char** optionsList, int optionsCount, int currentOptionIndex);
void updateMenuIndex(int* idx, int maxValue, int key);

void prepareTab(Cell cells[], int size, char tab[TAB_SIZE][TAB_SIZE]);
int goToNextStep(char tab[TAB_SIZE][TAB_SIZE]);
void drawTab(char tab[TAB_SIZE][TAB_SIZE], WINDOW* scr);
int randomBound(int lowerBound, int higherBound);

void cleanUp(WINDOW* scrList[SCREEN_NUMBER]);

int main(int argc, char *argv[]) 
{
	if(initscr() != NULL)
	{
		validateColors();
	
		initColorPairs();
		
		cbreak(); 
		noecho();
		
		nonl();
       //intrflush(stdscr, FALSE);
       //keypad(stdscr, TRUE);

		WINDOW* scrList[SCREEN_NUMBER];
		
		srand(time(NULL));
		createScreens(scrList);

		buildFrame(scrList[FRAME]);
		wrefresh(scrList[FRAME]);
		////////////////////////////////////////////////////////////////////////////
		//Main menu
		////////////////////////////////////////////////////////////////////////////
		int menuIdx = 0;
		
		buildMainMenu(scrList[MAIN_MENU], menuIdx);
		wrefresh(scrList[MAIN_MENU]);
		
		int key = wgetch(scrList[MAIN_MENU]);
		while (key != ENTER) 
		{
			if(key == UP || key == DOWN)
			{
				updateMenuIndex(&menuIdx, MAIN_MENU_OPTIONS-1, key);
				
				buildMainMenu(scrList[MAIN_MENU], menuIdx);
				
				wrefresh(scrList[MAIN_MENU]);
			}

			key = wgetch(scrList[MAIN_MENU]);
		}
		
		////////////////////////////////////////////////////////////////////////////
		//Input choice menu
		////////////////////////////////////////////////////////////////////////////
		if(menuIdx == 0)//Go to simulation
		{
			menuIdx = 0;
			
			wclear(scrList[MAIN_MENU]);
			
			buildInputMenu(scrList[INPUT_CHOICE], menuIdx);

			wrefresh(scrList[INPUT_CHOICE]);
			
			key = wgetch(scrList[INPUT_CHOICE]);
			while (key != ENTER) 
			{
				if(key == UP || key == DOWN)
				{
					updateMenuIndex(&menuIdx, INPUT_OPTIONS-1, key);
					
					buildInputMenu(scrList[INPUT_CHOICE], menuIdx);
					
					wrefresh(scrList[INPUT_CHOICE]);
				}

				key = wgetch(scrList[INPUT_CHOICE]);
			}
			
			////////////////////////////////////////////////////////////////////////////
			//Model choice menu
			////////////////////////////////////////////////////////////////////////////
			if(menuIdx == 1)//Go to model list
			{
				menuIdx = 0;
				
				wclear(scrList[INPUT_CHOICE]);
				
				buildModelMenu(scrList[MODEL_CHOICE], menuIdx);

				wrefresh(scrList[MODEL_CHOICE]);
				
				key = wgetch(scrList[MODEL_CHOICE]);
				while (key != ENTER) 
				{
					if(key == UP || key == DOWN)
					{
						updateMenuIndex(&menuIdx, MODEL_OPTIONS-1, key);
						
						buildModelMenu(scrList[MODEL_CHOICE], menuIdx);
						
						wrefresh(scrList[MODEL_CHOICE]);
					}

					key = wgetch(scrList[MODEL_CHOICE]);
				}
				
				////////////////////////////////////////////////////////////////////////////
				//Simulation screen
				////////////////////////////////////////////////////////////////////////////
				if(menuIdx != MODEL_OPTIONS-1)
				{
					wclear(scrList[MODEL_CHOICE]);
					wrefresh(scrList[MODEL_CHOICE]);
					
					mvwprintw(scrList[FRAME], LINES-2, (COLS - 67)/2, "'Escape' : Quit, 'Enter' : next step, 'Spacebar' : Toggle auto-mode");
					wrefresh(scrList[FRAME]);
					
					Cell* cells;
					int size = 0;	
					if(menuIdx == 0)//Cross model
					{
						size = 4;
						cells = malloc(size * sizeof(Cell));
					
						cells[0].x = 10;
						cells[0].y = 5;
						
						cells[1].x = 10;
						cells[1].y = 6;
						
						cells[2].x = 9;
						cells[2].y = 6;
											
						cells[3].x = 11;
						cells[3].y = 6;
					}
					else if (menuIdx == 1)//Pulsar model 
					{
						size = 48;
						cells = malloc(size * sizeof(Cell));
						int xCoordinates[] = {-4, -3, -2, 2, 3, 4, -6, -1, 1, 6, -6, -1, 1, 6, -6, -1, 1, 6, -4, 
												-3, -2, 2, 3, 4, -4, -3, -2, 2, 3, 4, -6, -1, 1, 6, -6, -1, 1, 6, -6, 
												-1, 1, 6, -4, -3, -2, 2, 3, 4};
						int yCoordinates[] = {-6, -6, -6, -6, -6, -6, -4, -4, -4, -4, -3, -3, -3, -3, -2, -2, -2,
												-2, -1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 
												4, 4, 4, 4, 6, 6, 6, 6, 6, 6};
						
						int i;
						for(i = 0; i<size; i++)
						{
							cells[i].x = xCoordinates[i] + 8;
							cells[i].y = yCoordinates[i] + 8;
						}
					}
					else if (menuIdx == 2)//Glider gun model 
					{
						size = 5;
						cells = malloc(size * sizeof(Cell));
						
						int xCoordinates[] = {6, 7, 5, 6, 7};
						int yCoordinates[] = {5, 6, 7, 7, 7};
												
						int i;
						for(i = 0; i<size; i++)
						{
							cells[i].x = xCoordinates[i] + 5;
							cells[i].y = yCoordinates[i] + 5;
						}
					}
					else if (menuIdx == 3)//Random model 
					{
						int maxSize = TAB_SIZE*TAB_SIZE;
						size = randomBound((int)(maxSize/4), maxSize);
						cells = malloc(size * sizeof(Cell));
						
						int i;
						for(i = 0; i<size; i++)
						{
							cells[i].x = randomBound(0, TAB_SIZE-1);
							cells[i].y = randomBound(0, TAB_SIZE-1);
						}
					}
					
					
					char tab[TAB_SIZE][TAB_SIZE];
					prepareTab(cells, size, tab);
					free(cells);
					
					drawTab(tab, scrList[SIMULATION]);
					key = 0;
					int automatic = 0;
					int step = 0;
					int aliveCells = 0;
					do
					{
						mvwprintw(scrList[FRAME], 3, (COLS - 67)/2, "Generation");
						mvwprintw(scrList[FRAME], 4, (COLS - 67)/2, "# %i", step);
						wrefresh(scrList[FRAME]);
						
						if(key == ENTER)
						{
							aliveCells = goToNextStep(tab);
																			drawTab(tab, scrList[SIMULATION]);
																			step++;
						}
						else if(key == SPACE)
						{
							if(automatic == 1)
							{
								wtimeout(scrList[SIMULATION], -1);
								automatic = 0;
							}
							else 
							{
								wtimeout(scrList[SIMULATION], 300);
								automatic = 1;
							}
						}

						key = wgetch(scrList[SIMULATION]);
						
						if(automatic == 1)
						{
							aliveCells = goToNextStep(tab);
							drawTab(tab, scrList[SIMULATION]);
							step++;
						}
					}while (key != ESCAPE && aliveCells > 0);
					
					wclear(scrList[SIMULATION]);
					mvwprintw(scrList[SIMULATION], 1, 2, "All cells are");
					mvwprintw(scrList[SIMULATION], 2, 4, "dead! :(");
					mvwprintw(scrList[SIMULATION], 4 , 0, "Press a key to quit");
					wrefresh(scrList[SIMULATION]);
					wgetch(scrList[SIMULATION]);
				}
			}
		}
		
		cleanUp(scrList);
		endwin();
	
		exit(EXIT_SUCCESS);
	}
	else 
	{
		error("Erreur a l'initialisation de l'ecran!", -1);
	}

	exit(EXIT_FAILURE);
}

/****************************************************************************
Error handling
****************************************************************************/
void error(char* message, int errorID)
{
	printf("\n\n>>> %s : %i\n\n", message, errorID);

	exit(EXIT_FAILURE);
}

/****************************************************************************
Color handling
****************************************************************************/
void validateColors()
{
	if (!has_colors()) 
	{
		endwin();
		error("Ce terminal ne supporte pas la couleur!", -1);
	}

	if (start_color() != OK) 
	{
        endwin();
        error("Erreur a l'initialisation des couleurs!", -1);
    }
}

void initColorPairs()
{	
	init_pair(WHITE_ON_BLACK, COLOR_WHITE, COLOR_BLACK);
	init_pair(RED_ON_BLACK, COLOR_RED, COLOR_BLACK);
	init_pair(GREEN_ON_BLACK, COLOR_GREEN, COLOR_BLACK);
	init_pair(BLUE_ON_BLACK, COLOR_BLUE, COLOR_BLACK);
	init_pair(YELLOW_ON_BLACK, COLOR_YELLOW, COLOR_BLACK);
	init_pair(PINK_ON_BLACK, COLOR_MAGENTA, COLOR_BLACK);
	init_pair(CYAN_ON_BLACK, COLOR_CYAN, COLOR_BLACK);
	init_pair(WHITE_ON_RED, COLOR_WHITE, COLOR_RED);
}

/****************************************************************************
Screen handling
****************************************************************************/
void createScreens(WINDOW* scrList[SCREEN_NUMBER])
{
	scrList[FRAME] = newwin(LINES, COLS, 0, 0);
	scrList[MAIN_MENU] = newwin(20, 41, 3, (COLS - 41)/2);
	scrList[INPUT_CHOICE] =  newwin(20, 41, 3, (COLS - 41)/2);
	scrList[MANUAL_INPUT] = newwin(20, 41, 3, (COLS - 41)/2);
	scrList[MODEL_CHOICE] = newwin(20, 41, 3, (COLS - 41)/2);
	scrList[SIMULATION] = newwin(20, 20, 2, (COLS - 20)/2);
	
	keypad(scrList[MAIN_MENU], TRUE);
	keypad(scrList[INPUT_CHOICE], TRUE);
	keypad(scrList[MODEL_CHOICE], TRUE);
}

void buildFrame(WINDOW* scr)
{
	box(scr, '|', '=');
	
	mvwprintw(scr, 1, (COLS - 41)/2, "THE ULTIMATE SUPER GAME OF LIFE IN ");
	wattron(scr, COLOR_PAIR(RED_ON_BLACK));
	waddch(scr, 'C');
	wattron(scr, COLOR_PAIR(GREEN_ON_BLACK));
	waddch(scr, 'O');
	wattron(scr, COLOR_PAIR(BLUE_ON_BLACK));
	waddch(scr, 'L');
	wattron(scr, COLOR_PAIR(YELLOW_ON_BLACK));
	waddch(scr, 'O');
	wattron(scr, COLOR_PAIR(PINK_ON_BLACK));
	waddch(scr, 'R');
	wattron(scr, COLOR_PAIR(CYAN_ON_BLACK));
	waddch(scr, 'S');
}

//Main menu
void buildMainMenu(WINDOW* scr, int selectedIndex)
{
	char* o1 = "- Go to the simulation";
	char* o2 = "- Quit";
	char* options[MAIN_MENU_OPTIONS] = {o1, o2};
	
	wclear(scr);
	buildGenericMenu(scr, options, MAIN_MENU_OPTIONS, selectedIndex);
	
	wattron(scr, COLOR_PAIR(WHITE_ON_BLACK));
	mvwprintw(scr, 7, 0, "Instructions :");
	mvwprintw(scr, 8, 0, "Use Up/Down arrow keys to select what youwant to do and hit Enter to confirm your choice.");
}

void buildInputMenu(WINDOW* scr, int selectedIndex)
{
	char* o1 = "- Manual input";
	char* o2 = "- Choose a model";
	char* o3 = "- Open file";
	char* o4 = "- Quit";
	char* options[INPUT_OPTIONS] = {o1, o2, o3, o4};
	
	wclear(scr);
	buildGenericMenu(scr, options, INPUT_OPTIONS, selectedIndex);
}

void buildModelMenu(WINDOW* scr, int selectedIndex)
{
	char* o1 = "- Cross";
	char* o2 = "- Pulsar";
	char* o3 = "- Glider";
	char* o4 = "- Random";
	char* o5 = "- Quit";
	char* options[MODEL_OPTIONS] = {o1, o2, o3, o4, o5};
	
	wclear(scr);
	buildGenericMenu(scr, options, MODEL_OPTIONS, selectedIndex);
}


/****************************************************************************
Menu handling
****************************************************************************/
void buildGenericMenu(WINDOW* scr, char** optionsList, int optionsCount, int currentOptionIndex)
{
	int i;
	for(i=0;i<optionsCount;i++)
	{
		if(i == currentOptionIndex)
		{
			wattron(scr, COLOR_PAIR(WHITE_ON_RED));
		}
		else 
		{
			wattron(scr, COLOR_PAIR(WHITE_ON_BLACK));
		}
		
		mvwprintw(scr, (i + 1), 0, optionsList[i]);
	}
}

void updateMenuIndex(int* idx, int maxValue, int key)
{
	if(key == DOWN)
	{
		if(*idx < maxValue)
		{
			(*idx)++;
		}
		else 
		{
			*idx = 0;
		}
	}
	else if (key == UP) 
	{
		if(*idx > 0)
		{
			(*idx)--;
		}
		else 
		{
			*idx = maxValue;
		}
	}
}
/****************************************************************************
Simulation handling
****************************************************************************/
void prepareTab(Cell cells[], int size, char tab[TAB_SIZE][TAB_SIZE])
{
	int x, y;

	for(y = 0; y< TAB_SIZE; y++)
	{
		for(x = 0; x< TAB_SIZE; x++)
		{
			tab[x][y] = DEAD_CELL;
		}
	}
	
	Cell cell;
	for(x = 0; x< size; x++)
	{
		cell = cells[x];

		tab[cell.x][cell.y] = LIVE_CELL;
	}
}

int goToNextStep(char tab[TAB_SIZE][TAB_SIZE])
{
	int x,y, yy, xx;
	char newTab[TAB_SIZE][TAB_SIZE];
	
	for(y = 0; y< TAB_SIZE; y++)
	{
		for(x = 0; x< TAB_SIZE; x++)
		{
			newTab[x][y] = DEAD_CELL;
		}
	}
	
	char newCell = LIVE_CELL;
	int border = TAB_SIZE-1;
	int nbNeigh = 0;

	for(y = 0; y< TAB_SIZE; y++)
	{
		for(x = 0; x< TAB_SIZE; x++)
		{
			nbNeigh = 0;
			
			if(tab[(x-1+TAB_SIZE) % TAB_SIZE][(y-1+TAB_SIZE) % TAB_SIZE] == LIVE_CELL)
			{
				nbNeigh++;
			}
			
			if(tab[x][(y-1+TAB_SIZE)%TAB_SIZE] == LIVE_CELL)
			{
				nbNeigh++;
			}

			if(tab[(x+1+TAB_SIZE) % TAB_SIZE][(y-1+TAB_SIZE)%TAB_SIZE] == LIVE_CELL)
			{
				nbNeigh++;
			}

			if(tab[(x+1+TAB_SIZE)%TAB_SIZE][y] == LIVE_CELL)
			{
				nbNeigh++;
			}

			if(tab[(x+1+TAB_SIZE) % TAB_SIZE][(y+1+TAB_SIZE)%TAB_SIZE] == LIVE_CELL)
			{
				nbNeigh++;
			}

			if(tab[x][(y+1+TAB_SIZE)%TAB_SIZE] == LIVE_CELL)
			{
				nbNeigh++;
			}

			if(tab[(x-1+TAB_SIZE) % TAB_SIZE][(y+1+TAB_SIZE)%TAB_SIZE] == LIVE_CELL)
			{
				nbNeigh++;
			}

			if(tab[(x-1+TAB_SIZE)%TAB_SIZE][y] == LIVE_CELL)
			{
				nbNeigh++;
			}
			
			//Update current cell in the new 'world' array
			//Any cell with fewer than two neighbours die
			//Any live cell with exactly two or three neighbours stays alive, if it's dead it becomes alive
			//Any cell with more three neighbours dies
			newCell = DEAD_CELL;
			if(nbNeigh == 3)
			{
				newCell = LIVE_CELL;
			}
			else if (nbNeigh == 2) 
			{
				if(tab[x][y] == LIVE_CELL)
					newCell = LIVE_CELL;
			}
			else
			{
				newCell = DEAD_CELL;
			}
			
			newTab[x][y] = newCell;
		}
	}
	
	//Copy new world back to display world
	int count = 0;
	for(y = 0; y< TAB_SIZE; y++)
	{
		for(x = 0; x< TAB_SIZE; x++)
		{
			tab[x][y] = newTab[x][y];
			
			if(tab[x][y] == LIVE_CELL)
				count++;
		}
	}
	
	return count;
}

void drawTab(char tab[TAB_SIZE][TAB_SIZE], WINDOW* scr)
{
	int x, y;
	
	wclear(scr);
	
	wattron(scr, COLOR_PAIR(GREEN_ON_BLACK));
	for(y = 0; y< TAB_SIZE; y++)
	{
		for(x = 0; x< TAB_SIZE; x++)
		{
			waddch(scr, tab[x][y]);
		}
	}

	wrefresh(scr);	
}

int randomBound(int lowerBound, int higherBound)
{
    return rand()%(higherBound+1-lowerBound) + lowerBound;
}
/****************************************************************************
Clean up handling
****************************************************************************/
void cleanUp(WINDOW* scrList[SCREEN_NUMBER])
{
	int idx;
	for (idx = 0; idx < SCREEN_NUMBER; idx++) 
	{
		delwin(scrList[idx]);
	}
}